7.5 Promise
在JavaScript的世界中,所有代码都是单线程执行的。
由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现:
function callback() {
console.log('Done');
}
console.log('before setTimeout()');
setTimeout(callback, 1000); // 1秒钟后调用callback函数
console.log('after setTimeout()');
观察上述代码执行,在Chrome的控制台输出可以看到:
before setTimeout()
after setTimeout()
(等待1秒后)
Done
可见,异步操作会在将来的某个时间点触发一个函数调用。
AJAX就是典型的异步操作:
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.status === 200) {
return success(request.responseText);
} else {
return fail(request.status);
}
}
}
把回调函数success(request.responseText)和fail(request.status)写到一个AJAX操作里很正常,但是不好看,而且不利于代码复用。
有没有更好的写法?比如写成这样:
let ajax = ajaxGet('http://...');
ajax.ifSuccess(success)
.ifFail(fail);
这种链式写法的好处在于,先统一执行AJAX逻辑,不关心如何处理结果,然后,根据结果是成功还是失败,在将来的某个时候调用success函数或fail函数。
古人云:“君子一诺千金”,这种“承诺将来会执行”的对象在JavaScript中称为Promise对象。
Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。先测试一下你的浏览器是否支持Promise:
Run
我们先看一个最简单的Promise例子:生成一个0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败:
function test(resolve, reject) {
let timeOut = Math.random() * 2;
log('set timeout to: ' + timeOut + ' seconds.');
setTimeout(function () {
if (timeOut < 1) {
log('call resolve()...');
resolve('200 OK');
}
else {
log('call reject()...');
reject('timeout in ' + timeOut + ' seconds.');
}
}, timeOut * 1000);
}
这个test()函数有两个参数,这两个参数都是函数,如果执行成功,我们将调用resolve('200 OK'),如果执行失败,我们将调用reject('timeout in ' + timeOut + ' seconds.')。可以看出,test()函数只关心自身的逻辑,并不关心具体的resolve和reject将如何处理结果。
有了执行函数,我们就可以用一个Promise对象来执行它,并在将来某个时刻获得成功或失败的结果:
let p1 = new Promise(test);
let p2 = p1.then(function (result) {
console.log('成功:' + result);
});
let p3 = p2.catch(function (reason) {
console.log('失败:' + reason);
});
变量p1是一个Promise对象,它负责执行test函数。由于test函数在内部是异步执行的,当test函数执行成功时,我们告诉Promise对象:
// 如果成功,执行这个函数:
p1.then(function (result) {
console.log('成功:' + result);
});
当test函数执行失败时,我们告诉Promise对象:
p2.catch(function (reason) {
console.log('失败:' + reason);
});
Promise对象可以串联起来,所以上述代码可以简化为:
new Promise(test).then(function (result) {
console.log('成功:' + result);
}).catch(function (reason) {
console.log('失败:' + reason);
});
可见Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了:
┌──────────────────┐
then │on_resolve(data) {│
┌──────────▶│ // TODO │
│ │} │
┌────────────────────────┐ └──────────────────┘
│ new Promise(async) │
└────────────────────────┘ ┌──────────────────┐
│ │ │on_reject(data) { │
│ └──────────▶│ // TODO │
promise│ catch │} │
│ └──────────────────┘
▼
┌────────────────────────┐
│async(resolve, reject) {│
│ // TODO │
│} │
└────────────────────────┘
Promise还可以做更多的事情,比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。
要串行执行这样的异步任务,不用Promise需要写一层一层的嵌套代码。有了Promise,我们只需要简单地写:
job1.then(job2).then(job3).catch(handleError);
其中,job1、job2和job3都是Promise对象。
除了串行执行若干异步任务外,Promise还可以并行执行异步任务。
试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()实现如下:
let p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
console.log(results); // 获得一个Array: ['P1', 'P2']
});
有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()实现:
let p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P1'
});
由于p1执行较快,Promise的then()将获得结果'P1'。p2仍在继续执行,但执行结果将被丢弃。
手撸代码
如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。
要理解这些 Promise 方法的实现,我们可以从最基础的 Promise 开始,然后逐步深入到 Promise.all、Promise.race 和 Promise.any。
首先需要说明的是,JavaScript 原生并没有 Promise.map 方法。通常所说的 Promise.map 是指对数组中的每个元素执行一个返回 Promise 的函数,并处理这些 Promise。Promise.all 就是实现这一功能的绝佳工具。
下面将通过代码示例,带你一步步了解它们的核心实现原理。
💡 Promise 基础
Promise 是一个代表异步操作最终完成(或失败)及其结果值的对象。一个简单的实现如下:
class SimplePromise {
constructor(executor) {
this.state = 'pending'; // 状态:pending, fulfilled, rejected
this.value = undefined; // 成功时的值
this.reason = undefined; // 失败时的原因
this.onFulfilledCallbacks = []; // 成功回调队列
this.onRejectedCallbacks = []; // 失败回调队列
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// 简化版 then 方法,处理状态和回调
if (this.state === 'fulfilled') {
onFulfilled(this.value);
}
if (this.state === 'rejected') {
onRejected(this.reason);
}
if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => onFulfilled(this.value));
this.onRejectedCallbacks.push(() => onRejected(this.reason));
}
}
}
在这段代码中,executor(执行器)是你传递给 Promise 构造函数的那个函数。
当你使用 new Promise((resolve, reject) => { ... }) 时,箭头函数 (resolve, reject) => { ... } 就是这个 executor。
你说得对,我们之前实现的 Promise 中的 executor 函数确实是同步执行的。但这并不矛盾,因为 Promise 的异步能力,并非来自于 executor 本身,而是来自于 executor 内部通常会调用的那些真正的异步 API。
简单来说,Promise 本身只是一个“包装器”或“管理器”,它负责管理和通知异步操作的结果,但它自己并不创造异步。
支持链式调用的promise
class ChainablePromise {
constructor(executor) {
this.state = 'pending'
this.value = undefined
this.reason = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
this.onFulfilledCallbacks.forEach((fn) => fn())
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.onRejectedCallbacks.forEach((fn) => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
// 核心改造:then 方法
then(onFulfilled, onRejected) {
// 处理未传入回调的情况 - 需要将函数设为默认或透传
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : (value) => value
onRejected =
typeof onRejected === 'function'
? onRejected
: (reason) => {
throw reason
}
// 1. 关键!每次调用 .then() 都必须返回一个新的 Promise
return new ChainablePromise((resolve, reject) => {
// 2. 定义一个函数来处理 onFulfilled 回调的执行和结果
const handleFulfillment = () => {
try {
// 执行用户传入的成功回调
const result = onFulfilled(this.value)
// 检查回调的返回值
if (result instanceof ChainablePromise) {
// 如果返回的是一个 Promise,就等待它完成
result.then(resolve, reject)
} else {
// 如果返回的是普通值,就用这个值 resolve 新的 Promise
resolve(result)
}
} catch (error) {
// 如果回调函数内部抛出异常,则 reject 新的 Promise
reject(error)
}
}
// 3. 定义一个函数来处理 onRejected 回调
const handleRejection = () => {
try {
const result = onRejected(this.reason)
if (result instanceof ChainablePromise) {
result.then(resolve, reject)
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
}
// 4. 根据当前 Promise 的状态,决定何时执行回调
if (this.state === 'fulfilled') {
handleFulfillment()
}
if (this.state === 'rejected') {
handleRejection()
}
if (this.state === 'pending') {
// 如果还是 pending 状态,就将回调函数存入队列,等待未来执行
this.onFulfilledCallbacks.push(handleFulfillment)
this.onRejectedCallbacks.push(handleRejection)
}
})
}
catch(onRejected) {
return this.then(null, onRejected)
}
finally(onFinally) {
return this.then(
(value) => {
onFinally()
return value
},
(reason) => {
onFinally()
throw reason
},
)
}
}
const myPromise = new ChainablePromise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 2000)
setTimeout(() => {
reject('error')
}, 3000)
})
.then((res) => {
console.log('success:', res)
})
.catch((err) => {
console.log('error:', err)
}).finally(() => {
console.log('finally')
})
一但递归起来就有点搞不明白了,呵呵呵
🤔 Promise 是如何实现异步的?
Promise 的异步能力是“借力”而来的。它依赖于 JavaScript 运行时环境(比如浏览器或 Node.js)提供的原生异步 API。
最常见的就是 setTimeout、XMLHttpRequest (或 fetch)、fs.readFile 等。
让我们来看一个例子:
const myPromise = new Promise((resolve, reject) => {
// 👆 executor 函数是同步执行的
console.log('1. executor 开始执行 (同步)');
// 2. 调用浏览器提供的异步 API
setTimeout(() => {
console.log('3. setTimeout 的回调执行 (异步)');
resolve('任务完成');
}, 1000);
console.log('2. executor 执行完毕 (同步)');
});
console.log('4. Promise 创建完毕,但异步任务还在后台进行');
myPromise.then(result => {
console.log('5. Promise 状态变为 fulfilled,then 回调执行', result);
});
我理解了,promise其实就是将我们还没有执行的逻辑放在了待执行的队列中,在pendding的状态结束的时候就回执行这些转态,太妙了。
执行顺序解析
executor同步执行:new Promise()被调用,executor函数立即开始执行。- 调用
setTimeout:executor内部调用了setTimeout。setTimeout是浏览器提供的一个异步 Web API。当setTimeout被调用时,它会告诉浏览器的计时器线程:“1秒后,把这个回调函数放进任务队列里”。然后,setTimeout函数本身会立即返回,不会阻塞后续代码。 executor执行完毕:executor中的同步代码执行完毕,Promise对象创建完成,状态仍为pending。- 主线程继续:主线程继续执行,打印第4行日志。
- 异步回调入队:1秒后,浏览器的计时器线程将
setTimeout的回调函数(也就是那个包含resolve('任务完成')的函数)放入任务队列 (Task Queue)。 - 事件循环 (Event Loop) 调度:JavaScript 的事件循环会不断检查调用栈是否为空。当主线程的同步任务都完成后,事件循环会从任务队列中取出回调函数,并将其放入调用栈执行。
resolve被调用:setTimeout的回调函数执行,调用了resolve。这时,Promise的状态才从pending变为fulfilled。then回调执行:Promise状态改变后,.then()方法注册的回调函数被放入微任务队列,等待执行,最终打印第5行日志。
📌 核心要点
executor是同步的:它的作用是立即启动一个异步任务。- 异步的来源:真正的异步行为是由
executor内部调用的原生异步 API(如setTimeout、fetch等)提供的。 Promise的角色:Promise是一个“状态机”和“回调管理器”。它通过resolve和reject函数,将异步操作的结果“通知”给后续的.then()或.catch()回调。
所以,Promise 之所以能处理异步,是因为它巧妙地包装了那些底层的、真正的异步机制,并提供了一套更优雅、更易用的链式调用语法。
🤝 Promise.all 实现
Promise.all 接收一个 Promise 可迭代对象(如数组),并返回一个新的 Promise。
- 成功:当所有传入的 Promise 都成功时,新 Promise 才会成功,结果是一个按原始顺序排列的值数组。
- 失败:只要有一个 Promise 失败,新 Promise 就会立即失败,并带上第一个失败的原因。
Promise.myAll = function(promises) {
return new Promise((resolve, reject) => {
// 1. 准备一个数组来按顺序存储结果
const results = new Array(promises.length);
// 2. 计数器,记录已完成的 Promise 数量
let completedCount = 0;
// 3. 遍历所有 Promise
promises.forEach((promise, index) => {
// 使用 Promise.resolve 包装,确保处理非 Promise 值
Promise.resolve(promise)
.then(value => {
// 4. 关键:根据原始索引存储结果,保证顺序
results[index] = value;
completedCount++;
// 5. 如果所有 Promise 都完成了,resolve 最终结果
if (completedCount === promises.length) {
resolve(results);
}
})
.catch(reject); // 6. 任何一个 Promise 失败,立即 reject
});
});
};
🏎️ Promise.race 实现
Promise.race 同样接收一个 Promise 可迭代对象,并返回一个新的 Promise。
- 竞速:返回的 Promise 会随着第一个 Promise 的敲定(无论是成功还是失败)而敲定。
Promise.myRace = function(promises) {
return new Promise((resolve, reject) => {
// 1. 遍历所有 Promise
promises.forEach(promise => {
// 2. 使用 Promise.resolve 包装,确保处理非 Promise 值
Promise.resolve(promise)
.then(resolve) // 3. 任何一个 Promise 成功,立即 resolve
.catch(reject); // 4. 任何一个 Promise 失败,立即 reject
});
});
};
✨ Promise.any 实现
Promise.any 是 ES2021 引入的新方法,它接收一个 Promise 可迭代对象,并返回一个新的 Promise。
- 成功:只要有一个 Promise 成功,新 Promise 就会立即成功,并带上第一个成功的值。
- 失败:只有当所有传入的 Promise 都失败时,新 Promise 才会失败,并带有一个
AggregateError,其中包含了所有失败的原因。
Promise.myAny = function(promises) {
return new Promise((resolve, reject) => {
const errors = []; // 1. 收集所有失败的原因
let rejectedCount = 0; // 2. 计数器,记录已失败的 Promise 数量
promises.forEach(promise => {
Promise.resolve(promise)
.then(resolve) // 3. 任何一个 Promise 成功,立即 resolve
.catch(error => {
// 4. 记录失败原因
errors.push(error);
rejectedCount++;
// 5. 只有当所有 Promise 都失败时,才 reject
if (rejectedCount === promises.length) {
// AggregateError 是 ES2021 内置的错误类型
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
});
};
📝 总结
| 方法 | 成功条件 | 失败条件 | 返回结果 |
|---|---|---|---|
| Promise.all | 所有 Promise 都成功 | 任一 Promise 失败 | 所有成功值的数组(按原始顺序) |
| Promise.race | 第一个 Promise 敲定(成功或失败) | 第一个 Promise 敲定(成功或失败) | 第一个敲定 Promise 的值或原因 |
| Promise.any | 第一个 Promise 成功 | 所有 Promise 都失败 | 第一个成功的值,或包含所有错误的 AggregateError |
正如我们之前提到的,JavaScript 原生并没有 Promise.map 方法。这个概念通常源于像 Bluebird 这样的第三方 Promise 库。 |
实现一个 Promise.map 的核心思路是:将一个数组,通过一个返回 Promise 的函数,转换成一个新的 Promise,这个新 Promise 的结果是一个由所有子 Promise 结果组成的数组。
我们可以利用之前讨论过的 Promise.all 来优雅地实现它。
🛠️ 实现 Promise.map
Promise.map 通常会接收两个参数:
- 一个数组(或可迭代对象)。
- 一个“映射函数”,该函数会对数组中的每个元素进行处理,并返回一个 Promise。
它的实现非常简单,核心就是将 Array.prototype.map 和 Promise.all 结合起来。
/**
* 实现一个 Promise.map 方法
* @param {Array} list - 需要处理的数组
* @param {Function} iteratee - 映射函数,接收 (item, index) 并应返回一个 Promise
* @returns {Promise} - 一个 Promise,resolve 时返回所有结果的数组
*/
Promise.myMap = function(list, iteratee) {
// 1. 使用原生 Array.map 遍历列表,对每个元素应用 iteratee 函数
// 这会生成一个由 Promise 组成的新数组
const promisesArray = list.map((item, index) => {
return iteratee(item, index);
});
// 2. 使用 Promise.all 等待这个 Promise 数组中的所有 Promise 完成
// Promise.all 返回的 Promise 会在所有子 Promise 成功后 resolve,结果为值数组
return Promise.all(promisesArray);
};
📝 使用示例
下面是一个具体的例子,模拟一个异步操作(例如从 API 获取数据),并使用我们实现的 Promise.myMap 来处理一个 ID 列表。
// 模拟一个异步函数,比如从服务器获取用户信息
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id: id, name: `用户${id}` });
} else {
reject(new Error(`无效的用户ID: ${id}`));
}
}, 1000);
});
}
const userIds = [1, 2, 3];
// 使用我们实现的 Promise.myMap
Promise.myMap(userIds, (id) => {
// 这里的函数会返回一个 Promise
console.log(`正在获取 ID 为 ${id} 的用户...`);
return fetchUser(id);
})
.then(users => {
console.log('所有用户获取成功:', users);
// 输出: 所有用户获取成功: [ { id: 1, name: '用户1' }, { id: 2, name: '用户2' }, { id: 3, name: '用户3' } ]
})
.catch(error => {
console.error('获取用户失败:', error);
// 如果任何一个 fetchUser 失败,这里会捕获到错误
});
✨ 进阶:增加并发限制
一个更强大的 Promise.map(如 Bluebird 库中的实现)通常会支持并发限制,即同时最多只执行 N 个 Promise。这对于处理大量任务,避免耗尽系统资源(如内存或网络连接)非常有用。
实现带并发限制的 Promise.map 会更复杂一些,需要一个队列来管理任务的执行。
/**
* 实现一个带并发限制的 Promise.map
* @param {Array} list - 需要处理的数组
* @param {Function} iteratee - 映射函数
* @param {Object} options - 选项,例如 { concurrency: 2 } 表示最多同时执行2个
* @returns {Promise}
*/
Promise.myMapWithConcurrency = function(list, iteratee, options = {}) {
const concurrency = options.concurrency || Infinity; // 默认无限制
const results = new Array(list.length);
let nextIndexToProcess = 0; // 下一个要处理的任务索引
let completedCount = 0;
return new Promise((resolve, reject) => {
// 启动第一批任务,数量不超过并发限制
function startNext() {
while (nextIndexToProcess < list.length && activeCount < concurrency) {
const index = nextIndexToProcess++;
activeCount++;
// 执行任务
Promise.resolve(iteratee(list[index], index))
.then(value => {
results[index] = value;
completedCount++;
activeCount--;
checkCompletion();
startNext(); // 当前任务完成,尝试启动下一个
})
.catch(reject); // 任何一个任务失败,整个 Promise.map 失败
}
}
function checkCompletion() {
if (completedCount === list.length) {
resolve(results);
}
}
let activeCount = 0;
// 开始执行
startNext();
});
};
📌 总结
- 基础实现:
Promise.map(list, fn)的核心就是Promise.all(list.map(fn))。它简单易用,但会同时启动所有异步任务。 - 进阶实现:带并发限制的
Promise.map更加健壮,适合在生产环境中处理大量任务,通过一个内部的任务队列和计数器来精确控制同时运行的 Promise 数量。